#include "thttpd.h"
#include "custom_handle.h"
#include <sys/types.h>
#include <sys/wait.h>
#include <ctype.h>

/**
 * 创建TCP服务器，并完成初始化
 * 参数：用于绑定的端口号
 * 返回值：完成listen后的网路套接字
 */
int init_server(int _port)
{
	// 传入了套接字
	int sock = socket(AF_INET, SOCK_STREAM, 0);
	if (sock < 0)
	{
		perror("socket failed");
		exit(2);
	}
	// exit(2) 和_exit(2) 和 return 2 的区别？
	// 	exit(2) 会进行清理工作（如调用 atexit 注册的函数和刷新输出缓冲区），然后终止程序。
	// _exit(2) 直接终止程序，不进行任何清理工作。
	// 在 main 函数中，return 2; 等同于 exit(2);，但在其他函数中，它只返回该函数。

	// 设置地址和端口号重用
	int opt = 1;
	setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
	// 设置端口号重用
	// int reuse =1;
	// setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse));

	struct sockaddr_in local;
	local.sin_family = AF_INET;
	local.sin_port = htons(_port);
	local.sin_addr.s_addr = INADDR_ANY;

	if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
	{
		perror("bind failed");
		exit(3);
	}

	if (listen(sock, 5) < 0)
	// 设置套接字的监听上限为5个
	//  listen 函数的第一个参数是套接字描述符（sock），
	//  第二个参数是监听队列的最大长度，即等待接受（accept）的客户端连接请求的最大数量。
	{
		perror("listen failed");
		exit(4);
	}

	// 将sock返回
	return sock;
}

static int get_line(int sock, char *buf) // 按行读取请求报头
{
	char ch = '\0';
	int i = 0;
	ssize_t ret = 0;
	while (i < SIZE && ch != '\n')
	{
		ret = recv(sock, &ch, 1, 0);
		if (ret > 0 && ch == '\r')
		{
			ssize_t s = recv(sock, &ch, 1, MSG_PEEK);
			if (s > 0 && ch == '\n')
			{
				recv(sock, &ch, 1, 0);
			}
			else
			{
				ch = '\n';
			}
		}
		buf[i++] = ch;
	}
	buf[i] = '\0';
	return i;
}

/**
 * 功能：清空消息报头
 * 参数：accepted 套接字
 * 返回值：无
 */
static void clear_header(int sock)
{
	char buf[SIZE];
	int ret = 0;
	do
	{
		ret = get_line(sock, buf);
	} while (ret != 1 && (strcmp(buf, "\n") != 0));
	// 通常表示成功读取了一行数据，并且这个正数可能表示读取的字节数（不包括行结束符，如果函数设计为排除它们的话）。
	// 为了读取到请求体（规避空行）
	// 在给出的 clear_header 函数中，目的是从一个套接字（socket）
	// sock中读取并丢弃HTTP头部，直到遇到HTTP消息体的开始
}

/**
 * 功能：404错误处理
 * 参数：accepted 套接字
 * 返回值：无
 */
static void show_404(int sock)
{
	clear_header(sock);
	char *msg = "HTTP/1.0 404	Not Found\r\n";
	send(sock, msg, strlen(msg), 0);	   // 发送状态行
	send(sock, "\r\n", strlen("\r\n"), 0); // 发送空行

	struct stat st;
	stat("wwwroot/404.html", &st);
	int fd = open("wwwroot/404.html", O_RDONLY);
	sendfile(sock, fd, NULL, st.st_size);
	close(fd);
}

/**
 * 错误处理函数：
 * 参数:accepted 套接字
 * 	   错误状态码
 * 无返回值
 */
void echo_error(int sock, int err_code)
{
	switch (err_code)
	{
	case 403:
		break;
	case 404:
		show_404(sock);
		break;
	case 405:
		break;
	case 500:
		break;
	defaut:
		break;
	}
}

/**
 * 功能：处理网页客户端的get无参数请求，处理非CGI的请求
 * 参数：accepted 套节字    path路径地址   文件的大小
 * 返回值：正常 0
 * sendfile方法发送文件失败返回 ret = 8
 * get无参只读文件失败返回  ret = 7
 */
static int echo_www(int sock, const char *path, size_t s)
{

	int fd = open(path, O_RDONLY);
	// 如果只读方式打开文件失败的话返回403状态码
	if (fd < 0)
	{
		echo_error(sock, 403);
		return 7;
	}
	// 只读方式打开文件成功，返回状态行：协议版本， 状态码 ， 状态码描述
	char *msg = "HTTP/1.0 200 OK\r\n";
	send(sock, msg, strlen(msg), 0);	   // 发送状态行
	send(sock, "\r\n", strlen("\r\n"), 0); // 发送空行

	// sendfile方法可以直接把文件发送到网络对端
	// 属于正文
	if (sendfile(sock, fd, NULL, s) < 0)
	{
		printf("get命令且无参，请求资源失败，sendfile err\n");
		// 返回状态码500，也就是服务器异常，运行错误
		echo_error(sock, 500);
		return 8;
	}
	// 关闭打开的路径下资源的文件描述符
	close(fd);
	return 0;
}

/**
 * 功能：用来处理post和get带参数的请求
 * 参数：accepted 套接字  method 请求方法  path资源路径  query_string GET方法的参数
 * 返回值：int 类型的 ret（退出状态）
 */
static int handle_request(int sock, const char *method,
						  const char *path, const char *query_string)
{
	char line[SIZE];

	int ret = 0;
	int content_len = -1;

	if (strcasecmp(method, "GET") == 0)
	// 如果是get请求
	{
		// 清空消息报头
		clear_header(sock);
	}
	else
	{
		// 获取post方法的数据 的 长度

		do // 一行一行的循环读取psot请求，为了从请求头部找寻"content-length"字段
		// Content-Length：作用：请求的内容长度：示例：Content-Length:348
		{
			ret = get_line(sock, line);
			if (strncasecmp(line, "content-length", 14) == 0)
			// content-length 正好 14个字节
			{
				content_len = atoi(line + 16);
				// 为什么是 16 呢？ 因为 Content-Length: 348
				// 多了一个冒号位： 一个空格位” “  14 + 2 = 16 ，调用atoi 将其转换成 int类型的数值
			}
		} while (ret != 1 && (strcmp(line, "\n") != 0));
		// 到这里就sock里面的数据已经只剩请求数据了（请求体）
	}

	printf("method = %s\n", method);			 // 打印请求方法
	printf("query_string = %s\n", query_string); // 打印 get 的参数
	printf("content_len = %d\n", content_len);	 // 打印 post 请求数据的长度

	char req_buf[4096] = {0};
	// 如果是POST方法，那么肯定携带请求数据，那么需要把数据解析出来
	if (strcasecmp(method, "POST") == 0)
	{
		// 将请求体的数据读出来放入到 req_buf中 ，读取的大小正好是请求体的大小
		int len = recv(sock, req_buf, content_len, 0);
		// 这里的打印语句只有post请求有，get请求是没有的
		printf("len = %d\n", len); // 打印请求数据的长度

		printf("req_buf = %s\n", req_buf); // 打印请求数据
	}

	// POST请求成功 ，发送成功状态行 200
	char *msg = "HTTP/1.1 200 OK\r\n\r\n";
	// 这里的写法包括空行
	send(sock, msg, strlen(msg), 0);

	// 请求交给自定义代码来处理，这是带参数的请求的真正核心的业务逻辑
	parse_and_process(sock, query_string, req_buf);
	
	return 0;
}

/**
 * 浏览器请求处理函数
 * 参数：用于建立TCP链接的accepted
 *
 */
int handler_msg(int sock)
{
	char del_buf[SIZE] = {}; // 用于接受原始的请求数据
	// SIZE  4096
	//  通常recv()函数的最后一个参数为0，代表从缓冲区取走数据
	//  而当为MSG_PEEK时代表只是查看数据，而不取走数据。
	recv(sock, del_buf, SIZE, MSG_PEEK);

#if 1 // 初学者强烈建议打开这个开关，看看tcp实际请求的协议格式
	puts("---------------------------------------");
	printf("recv:%s\n", del_buf);
	// 打印原始请求数据
	puts("---------------------------------------");
#endif
	// 接下来method方法判断之前的代码，可以不用重点关注
	// 知道是处理字符串，把需要的信息过滤出来即可
	char buf[SIZE];					 // 用于重装请求行的内容
	int count = get_line(sock, buf); // 获取第一行的内容的个数
	int ret = 0;
	char method[32];		   // 存放请求方法
	char url[SIZE];			   // 存放url
	char *query_string = NULL; // 存放get携带的参数
	int i = 0;
	int j = 0;
	int need_handle = 0; // 创建标志位

	// 第一个while循环用于获取请求方法
	while (j < count)
	{
		if (isspace(buf[j]))
		// 判断是否是空格，遇到空格后跳出循环
		{
			break;
		}
		method[i] = buf[j];
		// 存放请求方法，遇到空格时结束
		i++;
		j++;
	}
	method[i] = '\0'; // 将第一个空格位置0

	while (isspace(buf[j]) && j < SIZE)
	// 过滤空格 此时buf[j]是空格，需要跳过空格
	// SIZE buf的最大字节数，非空格的时候跳出循环
	{
		j++;
	}
	// 这里开始就开始判断发过来的请求是GET还是POST了
	if (strcasecmp(method, "POST") && strcasecmp(method, "GET"))
	//	strcasecmp(method, "POST") 是忽略大小写的比较
	// 含有任意一个请求方法结果都是0,截断法则，都不会执行if 里面的语句
	// 既不是post 也不是 get ，函数结束！！！！
	{
		printf("method failed\n"); // 如果都不是，那么提示一下
		echo_error(sock, 405);
		ret = 5;
		goto end; // 直接跳转结束
	}

	if (strcasecmp(method, "POST") == 0)
	// 如果是post指令，给need_handle 赋值
	{
		need_handle = 1;
	}

	i = 0; // 重置i的值为0，作为url数组的下标

	// 第三个while循环就是为了获取url，基本流程和第一个while循环一样
	while (j < count)
	{
		if (isspace(buf[j])) // 判断是否是空格
		{
			break;
		}
		if (buf[j] == '?')
		{
			// 因为请求中url最多只有一个‘？’，所以，该段的代码最多执行一次
			// 将资源路径（和附带数据，如果有的话）保存再url中，
			// 并且query_string指向附带数据
			query_string = &url[i]; // i =0 也就是数组的首地址
			query_string++;			// 指针向后移动一个位置,跳过问号,确定打印的首地址
			url[i] = '\0';			// 将问号位值置位成‘\0’,%s打印url的时候也就不会打印出？之后的参数了
		}
		else
		{
			url[i] = buf[j]; // 拿到url的
		}
		j++;
		i++;
	}
	url[i] = '\0';
	printf("URL = %s\n", url);
	// 打印url
	printf("query_string = %s\n", query_string);
	// 1 打印get 的 参数的值
	// 浏览器通过http://192.168.8.208:8080/?test=1234这种形式请求
	// 是携带参数的意思，那么就需要额外处理了
	if (strcasecmp(method, "GET") == 0 && query_string != NULL)
	// 是get且有参数的时候,给need_handle赋值为1；
	{
		need_handle = 1;
	}

	char path[SIZE];
	sprintf(path, "wwwroot%s", url);
	// 我们把请求资源的路径固定为wwwroot/下的资源，这个自己可以改
	// 因为我们存放我们网页文件.html的文件夹就是wwwroot
	printf("path = %s\n", path);
	// 2 固定资源的路径并且打印
	// 如果请求地址没有携带任何资源，那么默认返回login.html
	if (path[strlen(path) - 1] == '/')
	// 判断浏览器请求的是不是目录，也就是有没有请求具体的文件内容
	// 类似于在网页浏览器输入这样的网站：192.168.50.210 后面没有加任何的具体虚拟路径
	// 就会跳转到以下的默认网址，我这里设定的是登录界面
	{
		strcat(path, "login.html");
		// 定义默认网址 （寻常的设置默认网址都是 index.html）
	}

	// 如果请求的资源不存在，就要返回传说中的404页面了
	struct stat st;
	if (stat(path, &st) < 0)
	// 当时学模拟ls -l时候学过
	// 获取文件De属性
	// 获取客户端请求的资源的相关属性
	// stat 函数成功执行时返回 0；如果发生错误，则返回 -1，并设置全局变量 errno 以指示错误的类型。
	// 也就证明该路径不存在（文件不存在） 返回404
	{
		printf("can't find file\n");
		echo_error(sock, 404);
		ret = 6;
		goto end;
	}

	// 到这里基本就能确定是否需要自己的程序来处理后续请求了
	printf("need progress handle:%d\n", need_handle);
	// 是否需要调用线程处理函数

	// 如果是POST请求或者带参数的GET请求，就需要我们自己来处理了
	// 这些是业务逻辑，所以需要我们自己写代码来决定怎么处理
	if (need_handle)
	{
		printf("*******************************\n");
		ret = handle_request(sock, method, path, query_string);
	}
	else
	// 如果不是post并且是get而且没有参数；那就是纯粹的get无参数请求了
	// 直接打印空行并复制文件给对面的网页客户端
	{
		clear_header(sock);
		// 清空消息报头
		// 如果是GET方法，而且没有参数，则直接返回申请的网页资源
		ret = echo_www(sock, path, st.st_size);
		// 打印空行，相当于告知请求头部已经结束了
		// 接下来就是请求数据了
	}

/**
 * sendfile方法发送文件失败返回 ret = 8
 * get无参只读文件失败返回  ret = 7
 * 404 状态码跳转    ret = 6
 * 405 错误码进行跳转   ret = 5
 * 断开accepted链接
 * 返回值：根据返回值判断退出的方式
 */
end:
	close(sock);

	return ret;
}
